package edu.thu.thss.yxy.media;

import java.awt.Dimension;
import java.net.InetAddress;

import javax.media.*;
import javax.media.control.*;
import javax.media.format.*;
import javax.media.protocol.*;
import javax.media.rtp.*;

import edu.thu.thss.yxy.util.Logger;

/**
 * Class to send RTP using the JMF RTP API.
 * 
 * @author Andy Gao
 * 
 */
public class RtpTransmitter {
	protected static Logger logger = Logger.getLogger(RtpTransmitter.class);

	private Processor processor = null;
	
	/**
	 * An object that we use for.
	 */
	private final ProcessorUtility processorUtility = new ProcessorUtility();
	
	/**
	 * The rtpManager that handles audio and vedio streams in this session.
	 */
	private RTPManager rtpMgrs[];

	private DataSource dataOutput = null;
	private MediaSessionDescription mediaSessionDesc = null;
	
	private MediaManager mediaMgrCallback = null;
	
	public RtpTransmitter(String sessionDescription) {
		mediaSessionDesc = new MediaSessionDescription(sessionDescription);
		mediaSessionDesc.setAudioFormat("6");
		mediaSessionDesc.setVideoFormat("h263/rtp");
		mediaSessionDesc.setTransportProtocol("udp");
	}
	
	public RtpTransmitter(MediaSessionDescription mediaSessionDesc) {
		this.mediaSessionDesc = mediaSessionDesc;
	}
	
	public MediaManager getMediaManagerCallback() {
		return mediaMgrCallback;
	}

	public void setMediaManagerCallback(MediaManager mediaMgrCallback) {
		this.mediaMgrCallback = mediaMgrCallback;
	}

	/**
	 * Starts the transmission. Returns null if transmission started ok.
	 * Otherwise it returns a string with the reason why the setup failed.
	 * @throws MediaException 
	 */
	public synchronized String start() throws MediaException {
		String result;

		// Create a processor for the specified media locator
		// and program it to output JPEG/RTP
		result = createProcessor();
		if (result != null)
			return result;

		// Create an RTP session to transmit the output of the processor.
		result = createTransmitter();
		if (result != null) {
			processor.close();
			processor = null;
			return result;
		}

		// Start the transmission
		processor.start();

		return null;
	}

	private String createProcessor() throws MediaException {
		processor = mediaMgrCallback.getSourceProcessor();

		if (processor == null)
            throw new MediaException("Processor is null.", MediaException.SERVICE_NOT_STARTED);

        // Wait for it to configure
        boolean result = processorUtility.waitForState(processor, Processor.Configured);
        if (result == false)
            return "Couldn't configure processor";

        // Get the tracks from the processor
        TrackControl [] tracks = processor.getTrackControls();

        // Do we have atleast one track?
        if (tracks == null || tracks.length < 1)
            return "Couldn't find tracks in processor";

        // Set the output content descriptor to RAW_RTP
        // This will limit the supported formats reported from
        // Track.getSupportedFormats to only valid RTP formats.
        ContentDescriptor cd = new ContentDescriptor(ContentDescriptor.RAW_RTP);
        processor.setContentDescriptor(cd);

        Format supported[];
        Format chosen=null;
        boolean atLeastOneTrack = false;

        // Program the tracks.
        for (int i = 0; i < tracks.length; i++) {
            if (tracks[i].isEnabled()) {
               supported = tracks[i].getSupportedFormats();
               
                if (supported.length > 0) {
                    for(int j=0;j<supported.length;j++){
                        //System.out.println("Supported format : "+supported[j].toString().toLowerCase());
                        if (supported[j] instanceof VideoFormat) {
                            // For video formats, we should double check the
                            // sizes since not all formats work in all sizes.
                            if(mediaSessionDesc.getVideoFormat()!=null)
                                if(supported[j].toString().toLowerCase().indexOf(
                                		mediaSessionDesc.getVideoFormat().toLowerCase())!=-1)
                                    chosen = checkForVideoSizes(tracks[i].getFormat(), supported[j]);
                        } else {
                            if(mediaSessionDesc.getAudioFormat()!=null)
                                if(supported[j].toString().toLowerCase().indexOf(
                                		mediaSessionDesc.getAudioFormat().toLowerCase())!=-1)
                                    chosen = supported[j];  
                        }
                    }
                    if(chosen!=null){
                        tracks[i].setFormat(chosen);                
                        System.err.println("Track " + i + " is set to transmit as:");
                        System.err.println("  " + chosen);
                        atLeastOneTrack = true;
                    }
                } else{
                    tracks[i].setEnabled(false);
                }
            } else{
                tracks[i].setEnabled(false);
            }
        }

        if (!atLeastOneTrack){
            return "Couldn't set any of the tracks to a valid RTP format";
        }

        // Realize the processor. This will internally create a flow
        // graph and attempt to create an output datasource for JPEG/RTP
        // audio frames.
        result = processorUtility.waitForState(processor, Controller.Realized);
        if (result == false){
            return "Couldn't realize processor";
        }

        // Set the JPEG quality to .5.
        setJPEGQuality(processor, 0.25f);

        // Get the output data source of the processor
        dataOutput = processor.getDataOutput();

        return null;
	}

	private String createTransmitter() {
		// Cheated. Should have checked the type.
		PushBufferDataSource pbds = (PushBufferDataSource) dataOutput;
		PushBufferStream pbss[] = pbds.getStreams();

		rtpMgrs = new RTPManager[pbss.length];

		for (int i = 0; i < pbss.length; i++) {
			try {
				// New instance of RTPManager
				// to handle the RTP and RTCP transmission
				rtpMgrs[i] = RTPManager.newInstance();

				int destPort = mediaSessionDesc.getDestinationPort() + 2 * i;

				int localPort = mediaSessionDesc.getLocalPort() + 2 * i;
				// Establishing the connection with the remote host
				// with the chosen underlying protocol to RTP (either UDP)

				SessionAddress localAddress = new SessionAddress(InetAddress.getLocalHost(), localPort);
				rtpMgrs[i].initialize(localAddress);
				SessionAddress destAddress = new SessionAddress(InetAddress
						.getByName(mediaSessionDesc.getAddress()), destPort);
				rtpMgrs[i].addTarget(destAddress);

				System.err.println("Created RTP session: "
						+ mediaSessionDesc.getAddress() + " dest " + destPort);
				// Start the transmission with the remote host
				SendStream sendStream = rtpMgrs[i].createSendStream(dataOutput, i);
				sendStream.start();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		return null;
	}

	/**
	 * Stops the transmission if already started
	 */
	void stop() {
		synchronized (this) {
			if (processor != null) {
				processor.stop();
				processor.close();
				processor = null;
				if (rtpMgrs != null) {
					for (int i = 0; i < rtpMgrs.length; i++) {
						if (rtpMgrs[i] != null) {
							rtpMgrs[i].removeTargets("Session ended.");
							rtpMgrs[i].dispose();
							rtpMgrs[i] = null;
						}
					}
				}
			}
		}
	}
	
	 /**
     * For JPEG and H263, we know that they only work for particular
     * sizes.  So we'll perform extra checking here to make sure they
     * are of the right sizes.
     */
    private Format checkForVideoSizes(Format original, Format supported) {

        int width, height;
        Dimension size = ((VideoFormat)original).getSize();
        Format jpegFmt = new Format(VideoFormat.JPEG_RTP);
        Format h263Fmt = new Format(VideoFormat.H263_RTP);

        if (supported.matches(jpegFmt)) {
            // For JPEG, make sure width and height are divisible by 8.
            width = (size.width % 8 == 0 ? size.width :
                            (int)(size.width / 8) * 8);
            height = (size.height % 8 == 0 ? size.height :
                            (int)(size.height / 8) * 8);
        } else if (supported.matches(h263Fmt)) {
            // For H.263, we only support some specific sizes.
            if (size.width < 128) {
            width = 128;
            height = 96;
            } else if (size.width < 176) {
            width = 176;
            height = 144;
            } else {
            width = 352;
            height = 288;
            }
        } else {
            // We don't know this particular format.  We'll just
            // leave it alone then.
            return supported;
        }

        return (new VideoFormat(null, 
                                new Dimension(width, height), 
                                Format.NOT_SPECIFIED,
                                null,
                                Format.NOT_SPECIFIED)).intersects(supported);
    }
	
	/**
	 * Setting the encoding quality to the specified value on the JPEG encoder.
	 * 0.5 is a good default.
	 */
	void setJPEGQuality(Player p, float val) {
		Control cs[] = p.getControls();
		QualityControl qc = null;
		VideoFormat jpegFmt = new VideoFormat(VideoFormat.JPEG);

		// Loop through the controls to find the Quality control for
		// the JPEG encoder.
		for (int i = 0; i < cs.length; i++) {
			if (cs[i] instanceof QualityControl && cs[i] instanceof Owned) {
				Object owner = ((Owned) cs[i]).getOwner();

				// Check to see if the owner is a Codec.
				// Then check for the output format.
				if (owner instanceof Codec) {
					Format fmts[] = ((Codec) owner)
							.getSupportedOutputFormats(null);
					for (int j = 0; j < fmts.length; j++) {
						if (fmts[j].matches(jpegFmt)) {
							qc = (QualityControl) cs[i];
							qc.setQuality(val);
							System.err.println("- Setting quality to " + val
									+ " on " + qc);
							break;
						}
					}
				}
				if (qc != null)
					break;
			}
		}
	}

}
